Skip to content

fix(create-issue): emit deduplicate-by-title in compiled handler config#43345

Merged
pelikhan merged 5 commits into
mainfrom
copilot/fix-deduplicate-by-title-issue-another-one
Jul 4, 2026
Merged

fix(create-issue): emit deduplicate-by-title in compiled handler config#43345
pelikhan merged 5 commits into
mainfrom
copilot/fix-deduplicate-by-title-issue-another-one

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

deduplicate-by-title was parsed into CreateIssuesConfig but silently dropped from the emitted handler config — the field was never wired into the emission path. Additionally, GitHub Actions expression strings (e.g. ${{ inputs.dedup }}) were rejected by schema validation and unsupported by the existing AddBoolOrInt builder method.

Changes

  • New type TemplatableBoolOrInt (templatables.go): string-backed type with custom YAML/JSON marshaling that accepts booleans, integers 0–100, and GH Actions expression strings. Integers are range-validated at parse time; expressions are stored verbatim and emitted as JSON strings for runtime evaluation.

  • New builder method AddTemplatableBoolOrInt (compiler_safe_outputs_builder.go): parallel to existing AddTemplatableBool/AddTemplatableInt32.

  • CreateIssuesConfig.DeduplicateByTitle: changed from any*TemplatableBoolOrInt; handler registry updated to call AddTemplatableBoolOrInt.

  • Schema (main_workflow_schema.json): added templatable_bool_or_int $defs entry (bool | int 0–100 | expression string); deduplicate-by-title property updated to $ref it.

Example — all three forms now compile and emit correctly

safe-outputs:
  create-issue:
    deduplicate-by-title: true          # → {"deduplicate_by_title": true}
    deduplicate-by-title: 2             # → {"deduplicate_by_title": 2}
    deduplicate-by-title: ${{ inputs.dedup }}  # → {"deduplicate_by_title": "${{ inputs.dedup }}"}

Copilot AI and others added 2 commits July 4, 2026 08:28
…platableBoolOrInt

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
… field name)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix deduplicate-by-title option not emitted in handler config fix(create-issue): emit deduplicate-by-title in compiled handler config Jul 4, 2026
Copilot AI requested a review from pelikhan July 4, 2026 08:33
@pelikhan pelikhan marked this pull request as ready for review July 4, 2026 08:47
Copilot AI review requested due to automatic review settings July 4, 2026 08:47
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires safe-outputs.create-issue.deduplicate-by-title through to the emitted handler config and expands the configuration type system/schema to allow booleans, integers (0–100), and GitHub Actions expressions for that field.

Changes:

  • Added TemplatableBoolOrInt with custom YAML/JSON marshal/unmarshal to accept bool/int/expression forms and emit native JSON types for literals.
  • Added AddTemplatableBoolOrInt and updated create-issue handler config emission to include deduplicate_by_title.
  • Updated the main workflow JSON schema and added/updated tests covering bool/int/expression/nil cases.
Show a summary per file
File Description
pkg/workflow/templatables.go Introduces TemplatableBoolOrInt and related marshal/unmarshal helpers.
pkg/workflow/compiler_safe_outputs_builder.go Adds builder support for emitting TemplatableBoolOrInt into handler config JSON.
pkg/workflow/create_issue.go Changes CreateIssuesConfig.DeduplicateByTitle to *TemplatableBoolOrInt.
pkg/workflow/safe_outputs_handler_registry.go Wires deduplicate_by_title into the emitted handler config.
pkg/workflow/safe_outputs_config_generation_test.go Adds unit tests validating generated handler config JSON output for the new field.
pkg/workflow/compile_outputs_issue_test.go Adds compile-level regression tests to ensure the lockfile contains the emitted deduplicate_by_title.
pkg/parser/schemas/main_workflow_schema.json Adds $defs.templatable_bool_or_int and references it from deduplicate-by-title.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Low

if value == nil {
return b
}
b.config[key] = value.ToValue()
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (407 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/43345-templatable-bool-or-int-type-for-config-fields.md — review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-43345: Introduce TemplatableBoolOrInt for Config Fields

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 43345-templatable-bool-or-int-type-for-config-fields.md for PR #43345).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 50.8 AIC · ⌖ 10 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(create-issue): emit deduplicate-by-title in compiled handler config

Overall: The fix is correct and well-structured. The root cause (field parsed but silently dropped from the emitted handler config) is clearly addressed, and the three-form support (bool / int / GH Actions expression) is properly implemented with good test coverage.

Correctness

  • TemplatableBoolOrInt correctly handles YAML booleans (!!bool), integers (!!int with 0–100 range check), and expression strings (!!str with isExpression guard).
  • ToValue() and MarshalJSON() agree: "true"true, "false"false, numeric string → int, expression → string.
  • AddTemplatableBoolOrInt nil-guards correctly and mirrors the existing AddBoolOrInt builder pattern.
  • The handler registry swap from AddBoolOrIntAddTemplatableBoolOrInt is the minimal correct fix.
  • Schema $ref + sibling description is an established pattern throughout this schema — not a concern.

Non-blocking suggestion: The test suite covers true, 2, expression, and nil, but there is no test for explicit deduplicate-by-title: false. Setting false is valid YAML and exercises a distinct code path (ToValue() returns Go false; JSON emits false). A small companion test would close this gap.

Why this matters

false differs semantically from an absent field. The runtime may treat an explicit {"deduplicate_by_title":false} differently from the key being absent entirely. Without a test, a future refactor of ToValue() could silently break the false case. The implementation already handles it correctly; the test would just lock it in.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 46 AIC · ⌖ 4.96 AIC · ⊞ 4.9K

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 7 test(s): 7 design, 0 implementation, 0 violation(s).

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (Go: 7, JS: 0)
✅ Design 7 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 7 (100%)
Duplicate clusters 0
Inflation No (1.44:1 overall added lines)
🚨 Violations 0
Test File Classification Issues
TestDeduplicateByTitleBoolean compile_outputs_issue_test.go:499 design_test / behavioral_contract None
TestDeduplicateByTitleInteger compile_outputs_issue_test.go:537 design_test / behavioral_contract None
TestDeduplicateByTitleExpression compile_outputs_issue_test.go:577 design_test / behavioral_contract None
TestGenerateSafeOutputsConfigDeduplicateByTitleBool safe_outputs_config_generation_test.go design_test / behavioral_contract None
TestGenerateSafeOutputsConfigDeduplicateByTitleInt safe_outputs_config_generation_test.go design_test / behavioral_contract None
TestGenerateSafeOutputsConfigDeduplicateByTitleExpression safe_outputs_config_generation_test.go design_test / behavioral_contract None
TestGenerateSafeOutputsConfigDeduplicateByTitleNil safe_outputs_config_generation_test.go design_test / behavioral_contract None

Verdict

Passed. 0% implementation tests (threshold: 30%). All 7 new tests verify user-visible behavioral contracts for deduplicate-by-title serialization across all three value types (boolean, integer, expression) and the nil/absent case, both at the full compiler pipeline level and the config-generation unit level. Both test files have the mandatory //go:build !integration tag on line 1. No mock library violations found. Test inflation ratio is 1.44:1 (well under the 2:1 threshold).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 77.2 AIC · ⌖ 13.3 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — the fix is sound and well-structured, with a few small improvements worth addressing.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gap: TestDeduplicateByTitleBoolean only verifies true; the false code path in emission is untested
  • Logic duplication: MarshalJSON reimplements the same bool/int/string dispatch already present in ToValue
  • Minor test style: InDelta with delta 0 is less clear than assert.Equal(t, float64(2), ...)
  • Schema concern: sibling description alongside $ref may be silently ignored by draft-07 validators
  • No unit-level tests for invalid inputs: e.g. float JSON (1.5) or out-of-range integers rejected by UnmarshalJSON

Positive Highlights

  • ✅ Excellent root-cause fix: the bug was a genuine silent data-loss in the emission path
  • ✅ Clean new type design that mirrors the existing TemplatableBool / TemplatableInt32 patterns
  • ✅ Great test coverage for the happy paths (bool/int/expr/nil across both integration and unit layers)
  • ✅ Schema extension is minimal and well-placed in $defs
  • ✅ Wiring change in safe_outputs_handler_registry.go is a one-liner — appropriately surgical

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 96.5 AIC · ⌖ 11 AIC · ⊞ 6.6K
Comment /matt to run again

t.Fatal(err)
}

if !strings.Contains(string(lockContent), `\"deduplicate_by_title\":true`) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test name says "true/false is emitted correctly" but only true is exercised — false is never verified.

This is a meaningful edge case: false disables deduplication explicitly (vs. omitting the field), and the emission path has a separate code path for it. A regression that emits the string "false" instead of JSON boolean false would go undetected.

💡 Suggested extension

Add a companion test or subtest for deduplicate-by-title: false:

if !strings.Contains(string(lockContent), `\"deduplicate_by_title\":false`) {
    t.Errorf("Expected deduplicate_by_title:false in handler config, got:\n%s", lockContent)
}

@copilot please address this.


// MarshalJSON emits a JSON boolean for "true"/"false", a JSON integer for numeric
// strings, and a JSON string for GitHub Actions expressions.
func (t *TemplatableBoolOrInt) MarshalJSON() ([]byte, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] MarshalJSON and ToValue implement the same conversion logic independently — a future maintainer changing one is unlikely to update the other.

Both decode "true"/"false" to booleans and numeric strings to ints. Since ToValue already covers this logic, MarshalJSON could delegate to it:

💡 Suggested simplification
func (t *TemplatableBoolOrInt) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.ToValue())
}

This eliminates the duplication and makes future changes safer — only one switch needs updating.

@copilot please address this.

require.True(t, ok, "Expected create_issue key in config")

// JSON numbers unmarshal as float64 in map[string]any
assert.InDelta(t, float64(2), ciConfig["deduplicate_by_title"], 0, "deduplicate_by_title should be 2 (JSON number)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] assert.InDelta(..., 0, ...) with a delta of 0 is equivalent to == but uses float comparison — it works only because JSON unmarshals integers as float64 in map[string]any, which can be confusing.

The comment explains the float64 situation, but assert.Equal(t, float64(2), ciConfig["deduplicate_by_title"]) would be simpler and clearer than InDelta with a 0 tolerance.

💡 Alternative
assert.Equal(t, float64(2), ciConfig["deduplicate_by_title"], "deduplicate_by_title should be 2 (JSON number)")

This makes the intent unambiguous and matches how other numeric assertions in this file work (e.g. the existing assert.InDelta callers all pass a non-zero delta like 0.0001).

@copilot please address this.


// UnmarshalJSON allows TemplatableBoolOrInt to accept JSON booleans, JSON numbers,
// and JSON strings that are GitHub Actions expressions.
func (t *TemplatableBoolOrInt) UnmarshalJSON(data []byte) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] UnmarshalJSON has a subtle ordering issue: it tries bool first, then int. In Go's encoding/json, json.Unmarshal([]byte("1"), &boolVar) fails, so the sequence is correct — but the code silently falls through when the JSON contains a floating-point number like 1.5, which would fail the int branch and reject the input with a confusing error.

This is consistent with schema-declared constraints (integers only), but no test covers the 1.5 rejection case, so there's no regression guard.

💡 Suggestion

Add a test that verifies a float JSON input returns an error:

func TestTemplatableBoolOrIntUnmarshalJSONFloat(t *testing.T) {
    var v TemplatableBoolOrInt
    err := json.Unmarshal([]byte("1.5"), &v)
    require.Error(t, err, "float input should be rejected")
}

@copilot please address this.

}
]
"$ref": "#/$defs/templatable_bool_or_int",
"description": "Title-based deduplication for create-issue. Set to true for exact title matching, or provide a non-negative integer (0–100) to deduplicate by Levenshtein edit distance (e.g., 1 allows one-character differences). Accepts a GitHub Actions expression that resolves to a boolean or integer at runtime. Applies within-run and against open/recently-closed repository issues."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] Using both $ref and description on the same schema node may not behave as expected in all JSON Schema validators — the JSON Schema spec says sibling keywords alongside $ref are ignored in draft-07 (which is commonly used by editor tooling).

The description you want will likely be shadowed by the one in the templatable_bool_or_int $defs entry. Consider whether the field-specific description should live in the $defs entry itself, or if you need to use allOf to merge.

💡 Option

Move the field description into the $defs entry, or use allOf:

"deduplicate-by-title": {
  "allOf": [{ "": "#//templatable_bool_or_int" }],
  "description": "Title-based deduplication..."
}

Or keep the description only on the $defs definition and drop the override here.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · 233.4 AIC · ⌖ 5.29 AIC · ⊞ 5.4K
Comment /review to run again

t.Fatal(err)
}

if !strings.Contains(string(lockContent), `\"deduplicate_by_title\":true`) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing false coverage: the docstring says this test verifies true/false but only true is exercised; false (explicit disable) is never tested anywhere in the suite.

💡 Why this matters and what to add

false and nil have distinct semantics: nil omits the field entirely (handler uses its default), while false explicitly passes deduplicate_by_title: false to the handler, disabling deduplication even if the handler would otherwise default it on. The false path through both ToValue() and MarshalJSON() is trivially correct, but untested paths drift silently.

Add a compile-level check alongside the existing true test:

// TestDeduplicateByTitleFalse verifies that deduplicate-by-title: false emits the
// JSON boolean false (not omits the field).
func TestDeduplicateByTitleFalse(t *testing.T) {
    tmpDir := testutil.TempDir(t, "dedup-false-test")
    testContent := `---
on: workflow_dispatch
permissions:
  contents: read
engine: copilot
strict: false
safe-outputs:
  create-issue:
    max: 1
    deduplicate-by-title: false
---

Create test issues.
`
    // ... compile, read lock, assert:
    if !strings.Contains(string(lockContent), `\"deduplicate_by_title\":false`) {
        t.Errorf("Expected deduplicate_by_title:false in handler config, got:\n%s", lockContent)
    }
}

A parallel unit test in safe_outputs_config_generation_test.go (alongside TestGenerateSafeOutputsConfigDeduplicateByTitleBool) would also close the gap at the config-generation layer.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill and wrap up the remaining feedback in one pass before rerunning checks. Open items:

  • add an explicit deduplicate-by-title: false regression test
  • consider splitting the new x-strip-on-error tests for clearer failures and documenting the Go/JS StripOnErrorx-strip-on-error mapping
  • this PR still needs the ADR linked in the PR body as requested by Design Decision Gate

Generated by 👨‍🍳 PR Sous Chef · 8.28 AIC · ⌖ 5.03 AIC · ⊞ 4.3K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill and wrap up the remaining feedback in one pass before rerunning checks. Open items:

  • add an explicit deduplicate-by-title: false regression test
  • consid...

Addressed in a1f8e90: added explicit deduplicate-by-title: false coverage at both compile/config layers, tightened the bool-or-int runtime path for expression-derived string values, and cleaned up the related test/schema follow-ups. I did not make any x-strip-on-error changes because there are no such touched tests or mappings in this PR branch. The remaining ADR body link still needs a maintainer-side PR body edit from this environment — both gh pr edit and gh api returned 403s here.

Copilot AI requested a review from gh-aw-bot July 4, 2026 09:48
@pelikhan pelikhan merged commit 32f1273 into main Jul 4, 2026
37 checks passed
@pelikhan pelikhan deleted the copilot/fix-deduplicate-by-title-issue-another-one branch July 4, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create-issue: deduplicate-by-title is parsed but never emitted to the handler config

4 participants